CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュを削除(Invalidation)してみた
こんにちは、佐伯です。
CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュ削除(Invalidation)、キャッシュ削除のステータス確認、SNSへ通知までを行うLambda Functionを作ってみました。
CodePipelineからAWS Lambdaを呼び出すときどういう感じで作ればいいか?というのが本エントリの主旨です。
やってみた
書くこと
CodePipelineのアクション設定、CodePipelineから呼び出すLambda Functionのコードについて書きます。その他リソースの作成・設定手順やSlack通知のコードもググればいっぱい出てくると思うので省略させて頂きます!
構成
以下のようなS3にファイルを配置しCloudFrontで静的サイトを配信しているケースを想定してみました。
パイプライン処理は、CodeCommitにPushしたソースをCodePipelineでS3にデプロイ後、AWS Lambdaを呼び出しLambda FunctionでInvalidatioinとInvalidationのステータスチェックを行い、Invalidationが完了したらSNSへPublishする感じです。
CodePipelineからAWS Lambda呼び出し時の動作
CodePipelineからAWS Lambdaを呼び出す場合、CodePipelineはAWS Lambdaへジョブリクエストを送信します。Lambda Functionはジョブを実行し、実行結果をCodePipelineへ送信してジョブのステータスを知らせます。実行結果に継続トークンを含めることでジョブが完了していないことをCodePipelineへ知らせることができ、未完了のジョブはCodePipelineによって再実行される、といった仕組みのようです。
参考リンク: CodePipeline でのカスタムアクションの作成と追加 - CodePipeline
AWS Lambda の呼び出しアクションの制限事項
継続トークンを実行結果に含めることでCodePipelineがLambda Functionを再実行してくれますが、AWS Lambdaの呼び出しアクションのタイムアウトは1時間となっていますので、1時間以上かかる処理はAWS Lambda以外の方法で実現する必要があります。
参考リンク: AWS CodePipeline 制限 - CodePipeline
CodePipelineのアクション設定
CodePipelineのDeployステージの後にInvalidationステージを追加し、アクションを作成します。設定は以下の通りです。
ユーザーパラメーターにパイプライン名、CloudFrontディストリビューションID、SNSトピックARNの情報をJSONで設定しています。このパラメーターはCodePipelineからLambda Functionが呼び出される際にイベントとして渡され、Lambda Functionで参照することができます。以下のJSONは見やすいように改行していますが、実際の設定は改行を含めずに設定しています。
{ "PipelineName": "example-pipeline", "DistributionId": "E1EVDTCEXAMPLE", "SnsTopicArn": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:example-topic" }
Lambda Functionのサンプルコード
本題のCodePipelineから呼び出すLambda Functionのコードを書いてみました。ランタイムはPython3.7で動作することを確認しています。
import boto3 import json import logging import time import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) cp = boto3.client('codepipeline') cf = boto3.client('cloudfront') sns = boto3.client('sns') def create_invalidation(distribution_id): logger.info('Creating invalidation') res = cf.create_invalidation( DistributionId=distribution_id, InvalidationBatch={ 'Paths': { 'Quantity': 1, 'Items': ['/*'], }, 'CallerReference': str(time.time()) } ) invalidation_id = res['Invalidation']['Id'] logger.info('InvalidationId is %s', invalidation_id) return invalidation_id def monitor_invalidation_state(distribution_id, invalidation_id): res = cf.get_invalidation( DistributionId=distribution_id, Id=invalidation_id ) return res['Invalidation']['Status'] def put_job_success(job_id): logger.info('Putting job success') cp.put_job_success_result(jobId=job_id) def continue_job_later(job_id, invalidation_id): continuation_token = json.dumps({'InvalidationId':invalidation_id}) logger.info('Putting job continuation') cp.put_job_success_result( jobId=job_id, continuationToken=continuation_token ) def put_job_failure(job_id, err): logger.error('Putting job failed') message = 'Function exception: ' + str(err) cp.put_job_failure_result( jobId=job_id, failureDetails={ 'type': 'JobFailed', 'message': message } ) def sns_publish(sns_topic_arn, pipeline_name, job_id, job_status): logger.info('Publish to SNS topic') message = 'PipelineName: ' + pipeline_name + '\n' message += 'JobId: ' + job_id + '\n' message += 'Status: ' + job_status + '\n' res = sns.publish( TopicArn=sns_topic_arn, Message=message ) messaeg_id = res['MessageId'] logger.info('SNS Messaeg ID is %s', messaeg_id) def lambda_handler(event, context): try: job_id = event['CodePipeline.job']['id'] job_data = event['CodePipeline.job']['data'] user_parameters = json.loads( job_data['actionConfiguration']['configuration']['UserParameters'] ) pipeline_name = user_parameters['PipelineName'] distribution_id = user_parameters['DistributionId'] sns_topic_arn = user_parameters['SnsTopicArn'] if 'continuationToken' in job_data: continuation_token = json.loads(job_data['continuationToken']) invalidation_id = continuation_token['InvalidationId'] logger.info('InvalidationId is %s', invalidation_id) status = monitor_invalidation_state(distribution_id, invalidation_id) logger.info('Invalidation status is %s', status) if not status == 'Completed': continue_job_later(job_id, invalidation_id) else: sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='success') put_job_success(job_id) else: invalidation_id = create_invalidation(distribution_id) continue_job_later(job_id, invalidation_id) except Exception as err: logger.error('Function exception: %s', err) traceback.print_exc() sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='failed') put_job_failure(job_id, err) logger.info('Function complete') return "Complete."
以下AWSドキュメントにもサンプルコードがあり、こちらも参考になると思います。
参考リンク: CodePipeline で パイプラインに AWS Lambda 関数を呼び出す - CodePipeline
サンプルコードの補足
かなりざっくりですがLambda Functionの処理は以下のような流れになります。また、サンプルコードではInvalidationのPathは/*
に固定し、全キャッシュを削除する形にしていますので、参考にされる際はご注意ください。
- イベントからパイプライン名、CloudFrontディストリビューションID、SNSトピックARNを取得
- CloudFrontのキャッシュを削除を実行
- CodePipelineへ継続トークンを含めたジョブ実行結果を送信
- CodePipelineがLambda Functionを再実行
- イベントにcontinuationTokenが含まれている場合はCloudFrontのキャッシュ削除のステータスを確認
- キャッシュ削除が完了していない場合: CodePipelineへ継続トークンを含めた実行結果を送信
- キャッシュ削除が完了している場合: SNSトピックへメッセージをPublish
- キャッシュ削除が完了している場合: CodePipelineへ継続トークンなしで実行結果を送信
実行
適当にファイルを更新してパイプラインを実行した際のLambda Functionのログが以下となります。(各種IDなどは一部変更しています)
START RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample Version: $LATEST [INFO] 2019-03-06T05:25:08.878Z ab7c19c4-0552-4d9d-993c-f5deeexample Creating invalidation [INFO] 2019-03-06T05:25:09.908Z ab7c19c4-0552-4d9d-993c-f5deeexample InvalidationId is I22E4C6EXAMPLE [INFO] 2019-03-06T05:25:09.909Z ab7c19c4-0552-4d9d-993c-f5deeexample Putting job continuation [INFO] 2019-03-06T05:25:10.171Z ab7c19c4-0552-4d9d-993c-f5deeexample Function complete END RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample REPORT RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample Duration: 1330.52 ms Billed Duration: 1400 ms Memory Size: 128 MB Max Memory Used: 85 MB START RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example Version: $LATEST [INFO] 2019-03-06T05:25:40.610Z 5ff1fc7d-6b2d-444a-ba06-b9788example InvalidationId is I22E4C6EXAMPLE [INFO] 2019-03-06T05:25:41.593Z 5ff1fc7d-6b2d-444a-ba06-b9788example Invalidation status is InProgress [INFO] 2019-03-06T05:25:41.593Z 5ff1fc7d-6b2d-444a-ba06-b9788example Putting job continuation [INFO] 2019-03-06T05:25:41.721Z 5ff1fc7d-6b2d-444a-ba06-b9788example Function complete END RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example REPORT RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example Duration: 1118.73 ms Billed Duration: 1200 ms Memory Size: 128 MB Max Memory Used: 86 MB START RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example Version: $LATEST [INFO] 2019-03-06T05:26:13.941Z 91bfb2a8-ae58-42eb-ab0c-4bc16example InvalidationId is I22E4C6EXAMPLE [INFO] 2019-03-06T05:26:14.708Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Invalidation status is InProgress [INFO] 2019-03-06T05:26:14.708Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Putting job continuation [INFO] 2019-03-06T05:26:14.807Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Function complete END RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example REPORT RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example Duration: 887.00 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 86 MB START RequestId: 9ae598c1-66b7-4df9-a14d-4c623example Version: $LATEST [INFO] 2019-03-06T05:26:45.168Z 9ae598c1-66b7-4df9-a14d-4c623example InvalidationId is I22E4C6EXAMPLE [INFO] 2019-03-06T05:26:46.92Z 9ae598c1-66b7-4df9-a14d-4c623example Invalidation status is Completed [INFO] 2019-03-06T05:26:46.92Z 9ae598c1-66b7-4df9-a14d-4c623example Publish to SNS topic [INFO] 2019-03-06T05:26:46.301Z 9ae598c1-66b7-4df9-a14d-4c623example SNS Messaeg ID is d43d48e4-5d66-54ac-9e1c-1c1d0example [INFO] 2019-03-06T05:26:46.301Z 9ae598c1-66b7-4df9-a14d-4c623example Putting job success [INFO] 2019-03-06T05:26:46.428Z 9ae598c1-66b7-4df9-a14d-4c623example Function complete END RequestId: 9ae598c1-66b7-4df9-a14d-4c623example REPORT RequestId: 9ae598c1-66b7-4df9-a14d-4c623example Duration: 1280.82 ms Billed Duration: 1300 ms Memory Size: 128 MB Max Memory Used: 86 MB
AWSマネジメントコンソール上もCompleted
になっていました!
まとめ
CodePipelineからAWS Lambdaを呼び出す場合、アクションで設定したユーザーパラメーターをLambda Functionから参照することができます。それによって複数のパイプラインから同じLambda Functionを呼び出しても同様の処理を行うことができそうです。また、継続トークンを使用することで再実行までしてくれるCodePipeline最高!という気持ちになりました。
コーディングが必要でLambda Functionのメンテはしなくてはなりませんが、CodePipelineからAWS Lambdaを呼び出すことでCI/CDの幅が広がるのではないかと考えています。例えば、デプロイ後にAWS LambdaからのHTTPリクエストのテストを行ったり、Route 53のCNAME入れ替えを自動化したりなどのユースケースがあるかと思います。
本エントリの内容が少しでもお役にたてれば幸いです。